{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "# Quant Analyzer\n",
    "\n",
    "This notebook showcases a working code example of how to use AIMET to apply Quant Analyzer.\n",
    "Quant Analyzer is a feature which performs various analyses on a model to understand how each layer in the model responds to quantization.\n",
    "\n",
    "#### Overall flow\n",
    "This notebook covers the following\n",
    "1. Instantiate the example evaluation pipeline\n",
    "2. Load the FP32 model\n",
    "3. Apply QuantAnalyzer to the model\n",
    "\n",
    "\n",
    "#### What this notebook is not\n",
    "* This notebook is not designed to show state-of-the-art results.\n",
    "* For example, it uses a relatively quantization-friendly model like Resnet18.\n",
    "* Also, some optimization parameters are deliberately chosen to have the notebook execute more quickly."
   ]
  },
  {
   "cell_type": "markdown",
   "source": [
    "---\n",
    "## Dataset\n",
    "\n",
    "This notebook relies on the ImageNet dataset for the task of image classification. If you already have a version of the dataset readily available, please use that. Else, please download the dataset from appropriate location (e.g. https://image-net.org/challenges/LSVRC/2012/index.php#).\n",
    "\n",
    "**Note1**: The ImageNet dataset typically has the following characteristics and the dataloader provided in this example notebook rely on these\n",
    "- Subfolders 'train' for the training samples and 'val' for the validation samples. Please see the [pytorch dataset description](https://pytorch.org/vision/0.8/_modules/torchvision/datasets/imagenet.html) for more details.\n",
    "- A subdirectory per class, and a file per each image sample\n",
    "\n",
    "**Note2**: To speed up the execution of this notebook, you may use a reduced subset of the ImageNet dataset. E.g. the entire ILSVRC2012 dataset has 1000 classes, 1000 training samples per class and 50 validation samples per class. But for the purpose of running this notebook, you could perhaps reduce the dataset to say 2 samples per class. This exercise is left upto the reader and is not necessary.\n",
    "\n",
    "Edit the cell below and specify the directory where the downloaded ImageNet dataset is saved."
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "DATASET_DIR = '/path/to/dataset/'         # Please replace this with a real directory"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "---\n",
    "\n",
    "## 1. Example evaluation and training pipeline\n",
    "\n",
    "The following is an example training and validation loop for this image classification task.\n",
    "\n",
    "- **Does AIMET have any limitations on how the training, validation pipeline is written?**\n",
    "\n",
    "    Not really. We will see later that AIMET will modify the user's model to create a QuantizationSim model which is still a TensorFlow model.\n",
    "    This QuantizationSim model can be used in place of the original model when doing inference or training.\n",
    "\n",
    "- **Does AIMET put any limitation on the interface of the evaluate() or train() methods?**\n",
    "\n",
    "    Not really. You should be able to use your existing evaluate and train routines as-is."
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "import torch\n",
    "from Examples.common import image_net_config\n",
    "from Examples.torch.utils.image_net_evaluator import ImageNetEvaluator\n",
    "from Examples.torch.utils.image_net_data_loader import ImageNetDataLoader\n",
    "\n",
    "class ImageNetDataPipeline:\n",
    "\n",
    "    @staticmethod\n",
    "    def get_val_dataloader() -> torch.utils.data.DataLoader:\n",
    "        \"\"\"\n",
    "        Instantiates a validation dataloader for ImageNet dataset and returns it\n",
    "        \"\"\"\n",
    "        data_loader = ImageNetDataLoader(DATASET_DIR,\n",
    "                                         image_size=image_net_config.dataset['image_size'],\n",
    "                                         batch_size=image_net_config.evaluation['batch_size'],\n",
    "                                         is_training=False,\n",
    "                                         num_workers=image_net_config.evaluation['num_workers']).data_loader\n",
    "        return data_loader\n",
    "\n",
    "    @staticmethod\n",
    "    def evaluate(model: torch.nn.Module, use_cuda: bool) -> float:\n",
    "        \"\"\"\n",
    "        Given a torch model, evaluates its Top-1 accuracy on the dataset\n",
    "        :param model: the model to evaluate\n",
    "        :param use_cuda: whether or not the GPU should be used.\n",
    "        \"\"\"\n",
    "        evaluator = ImageNetEvaluator(DATASET_DIR, image_size=image_net_config.dataset['image_size'],\n",
    "                                      batch_size=image_net_config.evaluation['batch_size'],\n",
    "                                      num_workers=image_net_config.evaluation['num_workers'])\n",
    "\n",
    "        return evaluator.evaluate(model, iterations=None, use_cuda=use_cuda)"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "---\n",
    "\n",
    "## 2. Load the model"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "For this example notebook, we are going to load a pretrained resnet18 model from torchvision.\n",
    "Similarly, you can load any pretrained PyTorch model instead."
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "from torchvision.models import resnet18\n",
    "\n",
    "model = resnet18(pretrained=True)"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "AIMET quantization simulation requires the user's model definition to follow certain guidelines.\n",
    "For example, functionals defined in forward pass should be changed to equivalent torch.nn.Module.\n",
    "AIMET user guide lists all these guidelines.\n",
    "\n",
    "The following **ModelPreparer** API uses new graph transformation feature available in PyTorch 1.9+ version and automates model definition changes required to comply with the above guidelines."
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "from aimet_torch.model_preparer import prepare_model\n",
    "\n",
    "model = prepare_model(model)"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "---\n",
    "We should decide whether to place the model on a CPU or CUDA device.\n",
    "This example code will use CUDA if available in your current execution environment.\n",
    "You can change this logic and force a device placement if needed."
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "use_cuda = False\n",
    "if torch.cuda.is_available():\n",
    "    use_cuda = True\n",
    "    model.to(torch.device('cuda'))"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "---\n",
    "\n",
    "## 3. Apply QuantAnalyzer to the model\n",
    "\n",
    "QuantAnalyzer requires two functions to be defined by the user for passing data through the model:\n",
    "\n",
    "**Forward pass callback**\n",
    "\n",
    "One function will be used to pass representative data through a quantized version of the model to calibrate quantization parameters.\n",
    "This function should be fairly simple - use the existing train or validation data loader to extract some samples and pass them to the model.\n",
    "We don't need to compute any loss metrics, so we can just ignore the model output.\n",
    "\n",
    "The function **must** take two arguments, the first of which will be the model to run the forward pass on.\n",
    "The second argument can be anything additional which the function requires to run, and can be in the form of a single item or a tuple of items.\n",
    "\n",
    "If no additional argument is needed, the user can specify a dummy \"_\" parameter for the function.\n",
    "\n",
    "A few pointers regarding the forward pass data samples:\n",
    "\n",
    "- In practice, we need a very small percentage of the overall data samples for computing encodings.\n",
    "  For example, the training dataset for ImageNet has 1M samples. For computing encodings we only need 500 to 1000 samples.\n",
    "- It may be beneficial if the samples used for computing encoding are well distributed.\n",
    "  It's not necessary that all classes need to be covered since we are only looking at the range of values at every layer activation.\n",
    "  However, we definitely want to avoid an extreme scenario like all 'dark' or 'light' samples are used - e.g. only using pictures captured at night might not give ideal results.\n",
    "\n",
    "The following shows an example of a routine that passes unlabeled samples through the model for computing encodings.\n",
    "This routine can be written in many ways; this is just an example.\n",
    "This function only requires unlabeled data as no loss or other evaluation metric is needed."
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "def pass_calibration_data(sim_model, use_cuda):\n",
    "    data_loader = ImageNetDataPipeline.get_val_dataloader()\n",
    "    batch_size = data_loader.batch_size\n",
    "\n",
    "    if use_cuda:\n",
    "        device = torch.device('cuda')\n",
    "    else:\n",
    "        device = torch.device('cpu')\n",
    "\n",
    "    sim_model.eval()\n",
    "    samples = 1000\n",
    "\n",
    "    batch_cntr = 0\n",
    "    with torch.no_grad():\n",
    "        for input_data, target_data in data_loader:\n",
    "\n",
    "            inputs_batch = input_data.to(device)\n",
    "            sim_model(inputs_batch)\n",
    "\n",
    "            batch_cntr += 1\n",
    "            if (batch_cntr * batch_size) > samples:\n",
    "                break"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "In order to pass this function to QuantAnalyzer, we need to wrap it in a CallbackFunc object, as shown below.\n",
    "The CallbackFunc takes two arguments: the callback function itself, and the inputs to pass into the callback function."
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "from aimet_torch.v1.quant_analyzer import CallbackFunc\n",
    "\n",
    "forward_pass_callback = CallbackFunc(pass_calibration_data, use_cuda)"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "---\n",
    "\n",
    "**Evaluation callback**\n",
    "\n",
    "The second function will be used to evaluate the model, and needs to return an accuracy metric.\n",
    "In here, the user should pass any amount of data through the model which they would like when evaluating their model for accuracy.\n",
    "\n",
    "Like the forward pass callback, this function also must take exactly two arguments: the model to evaluate, and any additional argument needed for the function to work.\n",
    "The second argument can be a tuple of items in case multiple items are needed.\n",
    "\n",
    "We will be using the ImageNetDataPipeline's evaluate defined above for this purpose.\n",
    "Like the forward pass callback, we need to wrap the evaluation callback in a CallbackFunc object as well."
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "eval_callback = CallbackFunc(ImageNetDataPipeline.evaluate, use_cuda)"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "---\n",
    "\n",
    "**Enabling MSE loss per layer analysis**\n",
    "\n",
    "An optional analysis step in QuantAnalyzer calculates the MSE loss per layer in the model, comparing the layer outputs from the original FP32 model vs. a quantized model.\n",
    "To perform this step, the user needs to also provide an unlabeled DataLoader to QuantAnalyzer.\n",
    "\n",
    "We will demonstrate this step by using the ImageNetDataLoader imported above."
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "data_loader = ImageNetDataPipeline.get_val_dataloader()"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "---\n",
    "\n",
    "QuantAnalyzer also requires a dummy input to the model.\n",
    "This dummy input does not need to be representative of the dataset.\n",
    "All that matters is that the input shape is correct for the model to run on."
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "dummy_input = torch.rand(1, 3, 224, 224)    # Shape for each ImageNet sample is (3 channels) x (224 height) x (224 width)\n",
    "if use_cuda:\n",
    "    dummy_input = dummy_input.cuda()"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "---\n",
    "We are now ready to apply QuantAnalyzer."
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "from aimet_torch.v1.quant_analyzer import QuantAnalyzer\n",
    "\n",
    "quant_analyzer = QuantAnalyzer(model, dummy_input, forward_pass_callback, eval_callback)"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "To enable the MSE loss analysis, we set the following:"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "quant_analyzer.enable_per_layer_mse_loss(data_loader, num_batches=4)"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "Finally, to start the analyzer, we call .analyze().\n",
    "\n",
    "A few of the parameters are explained here:\n",
    "- **quant_scheme**:\n",
    "    - We set this to \"post_training_tf_enhanced\"\n",
    "      With this choice of quant scheme, AIMET will use the TF Enhanced quant scheme to initialize the quantization parameters like scale/offset.\n",
    "- **default_output_bw**: Setting this to 8 means that we are asking AIMET to perform all activation quantizations in the model using integer 8-bit precision.\n",
    "- **default_param_bw**: Setting this to 8 means that we are asking AIMET to perform all parameter quantizations in the model using integer 8-bit precision.\n",
    "\n",
    "There are other parameters that are set to default values in this example.\n",
    "Please check the AIMET API documentation of QuantizationSimModel to see reference documentation for all the parameters.\n",
    "\n",
    "When you call the analyze method, the following analyses are run:\n",
    "\n",
    "- Compare fp32 accuracy, accuracy with only parameters quantized, and accuracy with only activations quantized\n",
    "- For each layer, track the model accuracy when quantization for all other layers is disabled (enabling quantization for only one layer in the model at a time)\n",
    "- For each layer, track the model accuracy when quantization for all other layers is enabled (disabling quantization for only one layer in the model at a time)\n",
    "- Track the minimum and maximum encoding parameters calculated by each quantizer in the model as a result of forward passes through the model with representative data\n",
    "- When the TF Enhanced quantization scheme is used, track the histogram of tensor ranges seen by each quantizer in the model as a result of forward passes through the model with representative data\n",
    "- If enabled, track the MSE loss seen at each layer by comparing layer outputs of the original fp32 model vs. a quantized model"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "source": [
    "from aimet_common.defs import QuantScheme\n",
    "\n",
    "quant_analyzer.analyze(quant_scheme=QuantScheme.post_training_tf_enhanced,\n",
    "                       default_param_bw=8,\n",
    "                       default_output_bw=8,\n",
    "                       config_file=None,\n",
    "                       results_dir=\"./tmp/\")"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "AIMET will also output .html plots and json files where appropriate for each analysis to help visualize the data.\n",
    "\n",
    "The following output files will be produced, in a folder specified by the user:\n",
    "Output directory structure will be like below\n",
    "\n",
    "```\n",
    "results_dir\n",
    "|-- per_layer_quant_enabled.html\n",
    "|-- per_layer_quant_enabled.json\n",
    "|-- per_layer_quant_disabled.html\n",
    "|-- per_layer_quant_disabled.json\n",
    "|-- min_max_ranges\n",
    "|   |-- activations.html\n",
    "|   |-- activations.json\n",
    "|   |-- weights.html\n",
    "|   +-- weights.json\n",
    "|-- activations_pdf\n",
    "|   |-- name_{input/output}_{index_0}.html\n",
    "|   |-- name_{input/output}_{index_1}.html\n",
    "|   |-- ...\n",
    "|   +-- name_{input/output}_{index_N}.html\n",
    "|-- weights_pdf\n",
    "|   |-- layer1\n",
    "|   |   |-- param_name_{channel_index_0}.html\n",
    "|   |   |-- param_name_{channel_index_1}.html\n",
    "|   |   |-- ...\n",
    "|   |   +-- param_name_{channel_index_N}.html\n",
    "|   |-- layer2\n",
    "|   |   |-- param_name_{channel_index_0}.html\n",
    "|   |   |-- param_name_{channel_index_1}.html\n",
    "|   |   |-- ...\n",
    "|   |   +-- param_name_{channel_index_N}.html\n",
    "|   |-- ...\n",
    "|   |-- layerN\n",
    "|   |   |-- param_name_{channel_index_0}.html\n",
    "|   |   |-- param_name_{channel_index_1}.html\n",
    "|   |   |-- ...\n",
    "|   +-- +-- param_name_{channel_index_N}.html\n",
    "|-- per_layer_mse_loss.html\n",
    "+-- per_layer_mse_loss.json\n",
    "```\n",
    "\n",
    "#### Per-layer analysis by enabling/disabling quantization wrappers\n",
    "\n",
    "- per_layer_quant_enabled.html: A plot with layers on the x-axis and model accuracy on the y-axis, where each layer's accuracy represents the model accuracy when all quantizers in the model are disabled except for that layer's parameter and activation quantizers.\n",
    "- per_layer_quant_enabled.json: A json file containing the data shown in per_layer_quant_enabled.html, associating layer names with model accuracy.\n",
    "- per_layer_quant_disabled.html: A plot with layers on the x-axis and model accuracy on the y-axis, where each layer's accuracy represents the model accuracy when all quantizers in the model are enabled except for that layer's parameter and activation quantizers.\n",
    "- per_layer_quant_disabled.json: A json file containing the data shown in per_layer_quant_disabled.html, associating layer names with model accuracy.\n",
    "\n",
    "![per_layer_quant_enabled.html](./images/quant_analyzer_per_layer_quant_enabled.PNG)\n",
    "\n",
    "#### Encoding min/max ranges\n",
    "\n",
    "- min_max_ranges: A folder containing the following sets of files:\n",
    "    - activations.html: A plot with output activations on the x-axis and min-max values on the y-axis, where each output activation's range represents the encoding min and max parameters computed during forward pass calibration (explained below).\n",
    "    - activations.json: A json file containing the data shown in activations.html, associating layer names with min and max encoding values.\n",
    "    - weights.html: A plot with parameter names on the x-axis and min-max values on the y-axis, where each parameter's range represents the encoding min and max parameters computed during forward pass calibration.\n",
    "    - weights.json: A json file containing the data shown in weights.html, associating parameter names with min and max encoding values.\n",
    "\n",
    "![min_max_ranges.html](./images/quant_analyzer_min_max_ranges.PNG)\n",
    "\n",
    "#### PDF of statistics\n",
    "\n",
    "- (If TF Enhanced quant scheme is used) activations_pdf: A folder containing html files for each layer, plotting the histogram of tensor values seen for that layer's output activation seen during forward pass calibration.\n",
    "- (If TF Enhanced quant scheme is used) weights_pdf: A folder containing sub folders for each layer with weights.\n",
    "  Each layer's folder contains html files for each parameter of that layer, with a histogram plot of tensor values seen for that parameter seen during forward pass calibration.\n",
    "\n",
    "![weights_pdf.html](./images/quant_analyzer_weights_pdf.PNG)\n",
    "\n",
    "#### Per-layer MSE loss\n",
    "- (Optional, if per layer MSE loss is enabled) per_layer_mse_loss.html: A plot with layers on the x-axis and MSE loss on the y-axis, where each layer's MSE loss represents the MSE seen comparing that layer's outputs in the FP32 model vs. the quantized model.\n",
    "- (Optional, if per layer MSE loss is enabled) per_layer_mse_loss.json: A json file containing the data shown in per_layer_mse_loss.html, associating layer names with MSE loss.\n",
    "\n",
    "![per_layer_mse_loss.html](./images/quant_analyzer_per_layer_mse_loss.PNG)"
   ],
   "metadata": {
    "collapsed": false
   }
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 2
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython2",
   "version": "2.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
